In [1]:
%matplotlib inline
import nengo
import numpy as np
import pylab
This is meant as an example script to take a standard Nengo model and extract out the information needed to simulate it outside of Nengo. When developing a new backend, we often use this as a first step to figure out the process that would need to be automated.
In [2]:
model = nengo.Network()
with model:
def stim_a_func(t):
return np.sin(t*2*np.pi)
stim_a = nengo.Node(stim_a_func)
a = nengo.Ensemble(n_neurons=50, dimensions=1)
nengo.Connection(stim_a, a)
def stim_b_func(t):
return np.cos(t*np.pi)
stim_b = nengo.Node(stim_b_func)
b = nengo.Ensemble(n_neurons=50, dimensions=1)
nengo.Connection(stim_b, b)
c = nengo.Ensemble(n_neurons=200, dimensions=2, radius=1.5)
nengo.Connection(a, c[0])
nengo.Connection(b, c[1])
d = nengo.Ensemble(n_neurons=50, dimensions=1)
def multiply(x):
return x[0] * x[1]
nengo.Connection(c, d, function=multiply)
data = []
def output_function(t, x):
data.append(x)
output = nengo.Node(output_function, size_in=1)
nengo.Connection(d, output, synapse=0.03)
In [3]:
sim = nengo.Simulator(model)
sim.run(2)
pylab.plot(data)
Out[3]:
In [4]:
for ens in model.all_ensembles:
print('Ensemble %d' % id(ens))
print(' number of neurons: %d' % ens.n_neurons)
print(' tau_rc: %g' % ens.neuron_type.tau_rc)
print(' tau_ref: %g' % ens.neuron_type.tau_ref)
print(' bias: %s' % sim.data[ens].bias)
In [5]:
for node in model.all_nodes:
if node.size_out > 0:
print('Input node %d' % id(node))
print('Function to call: %s' % node.output)
In [6]:
for node in model.all_nodes:
if node.size_in > 0:
print('Output node %d' % id(node))
print('Function to call: %s' % node.output)
In [7]:
for conn in model.all_connections:
if isinstance(conn.pre_obj, nengo.Ensemble) and isinstance(conn.post_obj, nengo.Ensemble):
print('Connection from %d to %d' % (id(conn.pre_obj), id(conn.post_obj)))
print(' synapse time constant: %g' % conn.synapse.tau)
decoder = sim.data[conn].weights
transform = nengo.utils.builder.full_transform(conn, allow_scalars=False)
encoder = sim.data[conn.post_obj].scaled_encoders
print(' decoder: %s' % decoder)
print(' transform: %s' % transform)
print(' encoder: %s' % encoder)
In [8]:
for conn in model.all_connections:
if isinstance(conn.pre_obj, nengo.Node) and isinstance(conn.post_obj, nengo.Ensemble):
print('Connection from input %d to %d' % (id(conn.pre_obj), id(conn.post_obj)))
print(' synapse time constant: %g' % conn.synapse.tau)
transform = nengo.utils.builder.full_transform(conn, allow_scalars=False)
encoder = sim.data[conn.post_obj].scaled_encoders
print(' transform: %s' % transform)
print(' encoder: %s' % encoder)
In [9]:
for conn in model.all_connections:
if isinstance(conn.pre_obj, nengo.Ensemble) and isinstance(conn.post_obj, nengo.Node):
print('Connection from %d to output %d' % (id(conn.pre_obj), id(conn.post_obj)))
print(' synapse time constant: %g' % conn.synapse.tau)
decoder = sim.data[conn].weights
transform = nengo.utils.builder.full_transform(conn, allow_scalars=False)
print(' decoder: %s' % decoder)
print(' transform: %s' % transform)
In theory, the above information is all that is needed to run the model. The neurons are all LIF neurons with the stated refractory period, membrane time constant, and background input. They are scaled such that their reset voltage is 0 and their firing threshold is 1.0.
For connections, the code above gives a set of matrices. The actual connection weight matrix is defined by multiplying those matrices together. You can either implement them as a sequence of matrices (if your simulator supports this), or you can multiply them together to get a full weight matrix. For example, for the Connections between ensembles, you would do np.dot(encoder, np.dot(transform, decoder))
.
For inputs to the system, the input Node generally has a function. This function should be called, passing in the current time since the beginning of the simulation (in seconds) as t
. The result is the value that should be fed into the inputs to the neurons (after multiplying by the transform and the encoder).
For outputs from the system, the activity of all the neurons is multiplied by the decoder and the transform, resulting in a value x
. This value (and t
) is passed into the output node's function. Note that a Node can act as both input and output.
Each Connection also has a synapse value. This is an exponential filter that should be applied with the provided time constant.
The above considerations are enough to run this particular Nengo model, but there are a few things missing that may appear in other Nengo models
output_function
in the above example. Instead of making the output
Node in that above example, we could also have just done this and gotten the equivalence result:with model:
probe_d = nengo.Probe(d)
sim = nengo.Simulator(model)
sim.run(1)
pylab.plot(sim.data[probe_d])
with model:
def in_and_out_function(t, x):
return x[0] * x[2], x[1]
my_node = nengo.Node(in_and_out_function, size_in=3, size_out=2)
with model:
a = nengo.Ensemble(50, 1)
b_dummy = nengo.Node(None, size_in=1)
c_dummy = nengo.Node(None, size_in=1)
d = nengo.Ensemble(50, 1)
nengo.Connection(a, b_dummy, transform=[[1.0]])
nengo.Connection(b_dummy, c_dummy, transform=[[2.0]])
nengo.Connection(c_dummy, d, transform=[[3.0]])
with model:
a = nengo.Ensemble(50, 1)
d = nengo.Ensemble(50, 1)
nengo.Connection(a, d, transform=[[6.0]])
In [ ]: